package ru.compscicenter.uno.model;

import ru.compscicenter.uno.log.Logger;
import ru.compscicenter.uno.view.View;

import java.util.*;

/**
 * Created by IntelliJ IDEA.
 * User: Павел
 * Date: 15.10.11
 * Time: 1:53
 * Модель.
 */
public class Model implements IModel {
    private View view;

    private List<Card> takingDeck = Collections.synchronizedList(new LinkedList<Card>());
    private List<Card> droppedDeck = Collections.synchronizedList(new LinkedList<Card>());
    private List<Player> players = Collections.synchronizedList(new ArrayList<Player>());

    private Rules rules = new Rules();

    //число очков для победы

    //количество карт в руки при раздаче
    private static final int DEAL_COUNT = 7;

    //количество карт одного типа и одного цвета
    private static final int ZERO_CARDS_COUNT = 1;
    private static final int NUMERIC_CARDS_COUNT = 2;
    private static final int SKIP_CARDS_COUNT = 2;
    private static final int REVERSE_CARDS_COUNT = 2;
    private static final int TAKE_TWO_CARDS_COUNT = 2;
    private static final int WILD_CARDS_COUNT = 4;
    private static final int WILD_TAKE_FOUR_CARDS_COUNT = 4;
    private static final int ALL_CARDS_COUNT = 108;

    private boolean clockwise = true;
    private int currentPlayerId = 0;
    private int myPlayerId = 0;
    private int roundWinnerPlayerId = 0;
    private int gameWinnerPlayerId = 0;

    private Card.Color topColor;
    private boolean isNextPlayerSkip = false;
    private boolean isPlayerDroppedCard;
    private boolean isPlayerTookCard;
    private int nextPlayerShouldTake;
    private int currentPlayerShouldTake;

    public static enum GameState {
        NOT_STARTED, ROUND_PROCESS, ROUND_ENDED, GAME_ENDED
    }

    private GameState gameState = GameState.NOT_STARTED;

    public View getView() {
        return view;
    }

    public void setView(View view) {
        this.view = view;
    }

    private synchronized List<Card> generateDeck() {
        List<Card> result = Collections.synchronizedList(new LinkedList<Card>());

        int id = 0;
        for (int i = 0; i < ZERO_CARDS_COUNT; i++) {
            result.add(new Card(id++, Card.Type.NUM_0, Card.Color.BLUE));
            result.add(new Card(id++, Card.Type.NUM_0, Card.Color.GREEN));
            result.add(new Card(id++, Card.Type.NUM_0, Card.Color.RED));
            result.add(new Card(id++, Card.Type.NUM_0, Card.Color.YELLOW));
        }

        for (int j = 0; j < NUMERIC_CARDS_COUNT; j++) {
            for (int i = 1; i < 10; i++) {
                result.add(new Card(id++, Card.Type.get(i), Card.Color.BLUE));
                result.add(new Card(id++, Card.Type.get(i), Card.Color.GREEN));
                result.add(new Card(id++, Card.Type.get(i), Card.Color.RED));
                result.add(new Card(id++, Card.Type.get(i), Card.Color.YELLOW));
            }
        }

        for (int i = 0; i < SKIP_CARDS_COUNT; i++) {
            result.add(new Card(id++, Card.Type.SKIP, Card.Color.BLUE));
            result.add(new Card(id++, Card.Type.SKIP, Card.Color.GREEN));
            result.add(new Card(id++, Card.Type.SKIP, Card.Color.RED));
            result.add(new Card(id++, Card.Type.SKIP, Card.Color.YELLOW));
        }

        for (int i = 0; i < REVERSE_CARDS_COUNT; i++) {
            result.add(new Card(id++, Card.Type.REVERSE, Card.Color.BLUE));
            result.add(new Card(id++, Card.Type.REVERSE, Card.Color.GREEN));
            result.add(new Card(id++, Card.Type.REVERSE, Card.Color.RED));
            result.add(new Card(id++, Card.Type.REVERSE, Card.Color.YELLOW));
        }

        for (int i = 0; i < TAKE_TWO_CARDS_COUNT; i++) {
            result.add(new Card(id++, Card.Type.TAKE_TWO, Card.Color.BLUE));
            result.add(new Card(id++, Card.Type.TAKE_TWO, Card.Color.GREEN));
            result.add(new Card(id++, Card.Type.TAKE_TWO, Card.Color.RED));
            result.add(new Card(id++, Card.Type.TAKE_TWO, Card.Color.YELLOW));
        }

        for (int i = 0; i < WILD_CARDS_COUNT; i++) {
            result.add(new Card(id++, Card.Type.WILD, Card.Color.ANY));
        }

        for (int i = 0; i < WILD_TAKE_FOUR_CARDS_COUNT; i++) {
            result.add(new Card(id++, Card.Type.WILD_TAKE_FOUR, Card.Color.ANY));
        }

        return result;
    }

    public synchronized int getPointsToWin() {
        return rules.getPointsToWin();
    }

    public GameState getGameState() {
        return gameState;
    }

    public synchronized void setPointsToWin(int pointsToWin) {
        rules.setPointsToWin(pointsToWin);
    }

    public synchronized Rules getRules() {
        return rules;
    }

    public synchronized void setRules(Rules rules) {
        this.rules = rules;
    }

    public synchronized int getCurrentPlayerId() {
        return currentPlayerId;
    }

    public synchronized int getRoundWinnerPlayerId() {
        return roundWinnerPlayerId;
    }

    public synchronized int getGameWinnerPlayerId() {
        return gameWinnerPlayerId;
    }

    public synchronized void setCurrentPlayerId(int currentPlayerId) {
        this.currentPlayerId = currentPlayerId;
    }

    public synchronized int getMyPlayerId() {
        return myPlayerId;
    }

    public synchronized void setMyPlayerId(int myPlayerId) {
        this.myPlayerId = myPlayerId;
    }

    public synchronized List<Player> getPlayers() {
        return players;
    }

    private synchronized void changeDirection() {
        clockwise = !clockwise;
    }

    public synchronized boolean isClockwise() {
        return clockwise;
    }

    public synchronized Card getTopCard() {
        return droppedDeck.get(droppedDeck.size() - 1);
    }

    public synchronized Card.Color getTopColor() {
        return topColor;
    }

    public synchronized int getTakingDeckSize() {
        return takingDeck.size();
    }

    public synchronized int getDroppedDeckSize() {
        return droppedDeck.size();
    }

    public synchronized int getPlayersCount() {
        return players.size();
    }

    public synchronized List<Card> getTakingDeck() {
        return takingDeck;
    }

    public synchronized void setTakingDeck(LinkedList<Card> takingDeck) {
        this.takingDeck = takingDeck;
    }

    public synchronized List<Card> getDroppedDeck() {
        return droppedDeck;
    }

    public synchronized void setDroppedDeck(LinkedList<Card> droppedDeck) {
        this.droppedDeck = droppedDeck;
    }

    //ONLY FOR TESTS
    void setTopColor(Card.Color topColor) {
        this.topColor = topColor;
    }

    private synchronized boolean pushCard(Card card) {
        if (droppedDeck.isEmpty() || isCardCanBePlaced(card, getTopCard())) {
            droppedDeck.add(card);
            return true;
        }
        return false;
    }

    private synchronized boolean pushThrownCard(Card card) {
        if (isThrownCardCanBePlaced(card, getTopCard())) {
            droppedDeck.add(card);
            return true;
        }
        return false;
    }

    private synchronized boolean isCardCanBePlaced(Card card, Card top) {
        boolean isColorEqual = true;
        if (card.isColored()) {
            isColorEqual = (card.getColor().equals(topColor));
        }

        boolean isCardTypeEqual = card.getType().equals(top.getType());
        return isCardTypeEqual || isColorEqual;
    }

    private synchronized boolean isThrownCardCanBePlaced(Card card, Card top) {
        if (rules.isActivated(Rules.Rule.THROW_ONLY_NUMERIC)) {
            return card.isEqual(getTopCard()) && card.isNumeric();
        }

        boolean isColorEqual = true;
        if (card.isColored()) {
            isColorEqual = (card.getColor().equals(topColor));
        }

        boolean isCardTypeEqual = card.getType().equals(top.getType()) && card.getType() != Card.Type.WILD_TAKE_FOUR && card.getType() != Card.Type.WILD;

        return isCardTypeEqual && isColorEqual;
    }

    private synchronized Card takeCard() {
        if (takingDeck.isEmpty()) {
            throw new IllegalStateException("Taking card from empty deck");
        }

        Card card = takingDeck.get(0);
        takingDeck.remove(0);

        if (takingDeck.isEmpty()) {
            mergeDecks();
        }

        return card;
    }

    private synchronized void mergeDecks() {
        for (int i = 0; i < droppedDeck.size() - 1; i++) {
            Card card = droppedDeck.get(0);
            takingDeck.add(takingDeck.size(), card);
            droppedDeck.remove(0);
        }
    }

    private synchronized void dealCards() {
        for (Player player : players) {
            for (int i = 0; i < DEAL_COUNT; i++) {
                player.takeCard(takeCard());
            }
            //player.takeCard(getTopCard()); //use this line to debug startRound
        }
    }

    private synchronized void shuffleDeck(LinkedList<Card> cards) {
        Collections.shuffle(cards);
    }

    private synchronized void pushFirstCard() throws ModelException {
        if (!droppedDeck.isEmpty()) {
            throw new ModelException("Called first card pushing when dropped cards list is not empty");
        }

        Card firstCard = takeCard();
        if (firstCard.isNumeric()) {
            droppedDeck.add(firstCard);
            topColor = firstCard.getColor();
        } else {

            LinkedList<Card> tempDeck = new LinkedList<Card>(takingDeck);
            shuffleDeck(tempDeck);
            takingDeck = tempDeck;
            pushFirstCard(); //potentially infinity recursive
        }
    }

    public synchronized boolean startGame() throws ModelException {
        if (players.size() < 2) {
            return false;
        }

        for (Player player : players) {
            player.setPoints(0);
        }

        startRound();
        //todo
        return true;
    }

    public synchronized boolean startRound() throws ModelException {
        if (gameState == GameState.GAME_ENDED) {
            return false;
        }
        clockwise = true;
        gameState = GameState.ROUND_PROCESS;
        isNextPlayerSkip = false;
        isPlayerDroppedCard = false;
        isPlayerTookCard = false;
        nextPlayerShouldTake = 0;
        currentPlayerShouldTake = 0;
        droppedDeck.clear();
        takingDeck = generateDeck();
        LinkedList<Card> tempDeck = new LinkedList<Card>(takingDeck);
        shuffleDeck(tempDeck);
        takingDeck = tempDeck;
        pushFirstCard();
        dealCards();
        //todo
        return true;
    }

    public synchronized void setGameStateProcess() {
        gameState = GameState.ROUND_PROCESS;
    }

    public synchronized void setTopCard(Card card) {
        droppedDeck.add(card);
        topColor = card.getColor();
    }

    private synchronized void endRound(Player winner) {
        Logger.write("\n\n My ID : " + myPlayerId, Logger.Mode.ACTION);
        Logger.write("WINNER : " + StringRepresentation.getStringRepresentation(winner), Logger.Mode.ACTION);
        gameState = GameState.ROUND_ENDED;
        roundWinnerPlayerId = winner.getId();
        if (rules.isActivated(Rules.Rule.FIRST_LOOSER)) {
            for (Player player : players) {
                player.addPoints(player.getCurrentCardsPoints());
            }
        } else {
            int points = 0;
            for (Player player : players) {
                points += player.getCurrentCardsPoints();
            }
            winner.addPoints(points);
            Logger.write("WINNER ID: " + roundWinnerPlayerId, Logger.Mode.ACTION);
        }

        for (Player player : players) {
            player.dropAllCards();
        }

        if (rules.isActivated(Rules.Rule.FIRST_LOOSER)) {
            boolean isGameEnded = false;
            for (Player player : players) {
                if (player.getPoints() >= rules.getPointsToWin()) {
                    isGameEnded = true;
                    break;
                }
            }
            if (isGameEnded) {
                int minPoints = Integer.MAX_VALUE;
                for (Player player : players) {
                    if (player.getPoints() < minPoints) {
                        minPoints = player.getPoints();
                        gameWinnerPlayerId = player.getId();
                    }
                }
            }
        } else {
            Logger.write("WINNER 3: " + StringRepresentation.getStringRepresentation(winner), Logger.Mode.ACTION);
            if (winner.getPoints() >= rules.getPointsToWin()) {
                gameWinnerPlayerId = winner.getId();
                gameState = GameState.GAME_ENDED;
            }
        }
    }

    public synchronized boolean isRoundEnded() {
        return gameState == GameState.ROUND_ENDED;
    }

    public synchronized boolean isGameEnded() {
        return gameState == GameState.GAME_ENDED;
    }

    public synchronized boolean isGameRunning() {
        return gameState == GameState.ROUND_ENDED || gameState == GameState.ROUND_PROCESS;
    }

    public synchronized boolean doDropCard(int playerId, int cardId) throws ModelException {
        Player player = getPlayer(playerId);
        if (player == null) {
            throw new ModelException("Player + " + playerId + " is null");
        }
        Card card = player.getCard(cardId);
        if (card == null) {
            throw new ModelException("Card + " + cardId + " is null or not in arms of player " + playerId);
        }
        boolean success = doDropCard(playerId, cardId, card.getColor());
        if (view != null && success) {
            view.onDropCard(playerId, cardId);
        }
        return success;
    }

    public synchronized boolean doDropCard(int playerId, int cardId, Card.Color selectedColor) throws ModelException {
        Player player = getPlayer(playerId);
        if (player == null) {
            throw new ModelException("Player + " + playerId + " is null");
        }
        Card card = player.getCard(cardId);
        if (card == null) {
            throw new ModelException("Card + " + cardId + " is null or not in arms of player " + playerId);
        }

        if (rules.isActivated(Rules.Rule.EIGTH_EXTRA)) {
            if (isPlayerDroppedCard && !(getTopCard().getType() == Card.Type.NUM_8)) {
                return false;
            }
        } else {
            if (isPlayerDroppedCard) {
                return false;
            }
        }

        if (rules.isActivated(Rules.Rule.TRANSFER) && currentPlayerShouldTake > 0) {
            if (!(card.getType() == Card.Type.TAKE_TWO || card.getType() == Card.Type.WILD_TAKE_FOUR)) {
                return false;
            }
        }

        if (!rules.isActivated(Rules.Rule.THROW)) {
            if (playerId != currentPlayerId) {
                return false;
            }
        }

        if (selectedColor != Card.Color.RED &&
                selectedColor != Card.Color.GREEN &&
                selectedColor != Card.Color.BLUE &&
                selectedColor != Card.Color.YELLOW) {
            return false;
        }

        boolean success;
        if (rules.isActivated(Rules.Rule.THROW) && playerId != currentPlayerId) {
            success = pushThrownCard(card);
        } else {
            success = pushCard(card);
        }
        if (!success) {
            return false;
        } else
            player.dropCard(card);
        topColor = selectedColor;

        if (card.getType() == Card.Type.REVERSE) {
            changeDirection();
        }

        nextPlayerShouldTake = currentPlayerShouldTake;
        currentPlayerShouldTake = 0;
        if (card.getType() == Card.Type.TAKE_TWO) {
            nextPlayerShouldTake += 2;
        } else if (card.getType() == Card.Type.WILD_TAKE_FOUR) {
            nextPlayerShouldTake += 4;
        }
        if (!rules.isActivated(Rules.Rule.TRANSFER)) {
            Player nextPlayer = getPlayer(nextPlayerId());
            for (int i = 0; i < nextPlayerShouldTake; i++) {
                nextPlayer.takeCard(takeCard());
            }
            nextPlayerShouldTake = 0;

            if (card.getType() == Card.Type.WILD_TAKE_FOUR || card.getType() == Card.Type.SKIP || card.getType() == Card.Type.TAKE_TWO) {
                isNextPlayerSkip = true;
            }
        }
        if (view != null && success) {
            view.onDropCard(playerId, cardId, selectedColor);
        }
        isPlayerDroppedCard = true;

        if (rules.isActivated(Rules.Rule.THROW)) {
            //после подкидывания ходит игрок после подкинувшего.
            currentPlayerId = playerId;
        }
        doEndTurn(currentPlayerId);
        if (view != null && success) {
            view.onEndTurn(currentPlayerId);
        }
        return true;
    }

    public synchronized boolean doTakeCard(int playerId) throws ModelException {
        Player player = getPlayer(playerId);
        if (player == null) {
            throw new ModelException("Player + " + playerId + " is null");
        }

        if (playerId != currentPlayerId) {
            return false;
        }

        if (isPlayerTookCard) {
            return false;
        }

        if (rules.isActivated(Rules.Rule.TRANSFER) && currentPlayerShouldTake > 0) {
            for (int i = 0; i < currentPlayerShouldTake; i++) {
                player.takeCard(takeCard());
            }
            currentPlayerShouldTake = 0;
            isPlayerTookCard = true;
            doEndTurn(currentPlayerId);
        } else {
            player.takeCard(takeCard());
            isPlayerTookCard = true;
        }
        if (view != null) {
            view.onTakeCard(playerId);
        }
        return true;
    }

    public synchronized boolean doSayUno(int playerId) throws ModelException {
        Player player = getPlayer(playerId);
        if (player == null) {
            throw new ModelException("Player + " + playerId + " is null");
        }

        if (player.getCardsCount() != 1) {
            return false;
        }

        player.sayUno();
        if (view != null) {
            view.onSayUno(playerId);
        }
        return true;
    }

    public synchronized boolean doSomebodyDidntSayUno(int playerId, int whoDidntSayUnoId) throws ModelException {
        Player player = getPlayer(playerId);
        Player whoDidntSayUno = getPlayer(whoDidntSayUnoId);
        if (player == null) {
            throw new ModelException("Player + " + playerId + " is null");
        }
        if (whoDidntSayUno == null) {
            throw new ModelException("Player who didn't say Uno + " + whoDidntSayUnoId + " is null");
        }

        if (whoDidntSayUno.getCardsCount() == 1 && !whoDidntSayUno.isSaidUno()) {
            whoDidntSayUno.takeCard(takeCard());
            whoDidntSayUno.takeCard(takeCard());
            if (view != null) {
                view.onSomebodyDidntSayUno(playerId, whoDidntSayUnoId);
            }
            return true;
        }
        return false;
    }

    private synchronized boolean isPlayerDidSomething() {
        return isPlayerDroppedCard || isPlayerTookCard;
    }

    public synchronized boolean isIDidSomething() {
        return currentPlayerId == myPlayerId && isPlayerDidSomething();
    }

    public synchronized boolean doEndTurn(int playerId) {
        if (playerId != currentPlayerId) {
            return false;
        }

        if (!isPlayerDidSomething()) {
            return false;
        }

        if (currentPlayerShouldTake > 0) {
            return false;
        }

        switchToNextPlayer();
        if (isNextPlayerSkip) {
            isNextPlayerSkip = false;
            switchToNextPlayer();
        }

        checkGameState();
        isPlayerDroppedCard = false;
        isPlayerTookCard = false;
        currentPlayerShouldTake = nextPlayerShouldTake;
        nextPlayerShouldTake = 0;
        if (view != null) {
            view.onEndTurn(playerId);
        }
        return true;
    }

    private synchronized void checkGameState() {
        synchronized (players) {
            for (Player player : players) {
                if (player.getCardsCount() == 0) {
                    endRound(player);
                    break;
                }
            }
        }
    }

    public synchronized boolean addPlayer(Player player) {
        if (player == null) {
            throw new IllegalArgumentException("Adding null player");
        }

        if (players.size() + 1 > 8) {
            return false;
        }
        //player.setId(players.size());
        players.add(player);
        return true;
    }

    public synchronized boolean removePlayer(int playerId) {
        return players.remove(getPlayer(playerId));
    }

    private synchronized void switchToNextPlayer() {
        currentPlayerId = nextPlayerId();
    }

    public synchronized Player getPlayer(int id) {
        for (Player player : players) {
            if (player.getId() == id) {
                return player;
            }
        }
        return null;
    }

    public synchronized int nextPlayerId() {
        return nextPlayerId(currentPlayerId);
    }

    public synchronized int nextPlayerId(int playerId) {
        if (clockwise) {
            playerId++;
            if (playerId >= getPlayersCount()) {
                playerId = 0;
            }
        } else {
            playerId--;
            if (playerId < 0) {
                playerId = getPlayersCount() - 1;
            }
        }
        return playerId;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Model model = (Model) o;

        if (clockwise != model.clockwise) return false;
        if (currentPlayerId != model.currentPlayerId) return false;
        if (currentPlayerShouldTake != model.currentPlayerShouldTake) return false;
        if (gameWinnerPlayerId != model.gameWinnerPlayerId) return false;
        if (isNextPlayerSkip != model.isNextPlayerSkip) return false;
        if (isPlayerDroppedCard != model.isPlayerDroppedCard) return false;
        if (isPlayerTookCard != model.isPlayerTookCard) return false;
        if (myPlayerId != model.myPlayerId) return false;
        if (nextPlayerShouldTake != model.nextPlayerShouldTake) return false;
        if (roundWinnerPlayerId != model.roundWinnerPlayerId) return false;
        if (droppedDeck != null ? !droppedDeck.equals(model.droppedDeck) : model.droppedDeck != null) return false;
        if (gameState != model.gameState) return false;
        if (players != null ? !players.equals(model.players) : model.players != null) return false;
        if (rules != null ? !rules.equals(model.rules) : model.rules != null) return false;
        if (takingDeck != null ? !takingDeck.equals(model.takingDeck) : model.takingDeck != null) return false;
        if (topColor != model.topColor) return false;
        if (view != null ? !view.equals(model.view) : model.view != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = view != null ? view.hashCode() : 0;
        result = 31 * result + (takingDeck != null ? takingDeck.hashCode() : 0);
        result = 31 * result + (droppedDeck != null ? droppedDeck.hashCode() : 0);
        result = 31 * result + (players != null ? players.hashCode() : 0);
        result = 31 * result + (rules != null ? rules.hashCode() : 0);
        result = 31 * result + (clockwise ? 1 : 0);
        result = 31 * result + currentPlayerId;
        result = 31 * result + myPlayerId;
        result = 31 * result + roundWinnerPlayerId;
        result = 31 * result + gameWinnerPlayerId;
        result = 31 * result + (topColor != null ? topColor.hashCode() : 0);
        result = 31 * result + (isNextPlayerSkip ? 1 : 0);
        result = 31 * result + (isPlayerDroppedCard ? 1 : 0);
        result = 31 * result + (isPlayerTookCard ? 1 : 0);
        result = 31 * result + nextPlayerShouldTake;
        result = 31 * result + currentPlayerShouldTake;
        result = 31 * result + (gameState != null ? gameState.hashCode() : 0);
        return result;
    }
}